package compilador.lexico;

/**
 * Faz o reconhecimento de tokens do arquivo de entrada.
 *
 * @author Carlos H V Pinto
 * @author Lucas S Bueno
 * @author Paulo G L Freire
 */
public final class AnalisadorLexico {

    private TabelaReservados tpr;
    private LeitorEntrada leitor;

    /**
     * Cria um Analisador Léxico com a tabela de palavras/simbolos reservados e
     * um leitor de entrada especificado.
     *
     * @param leitor leitor de entrada utilizado para percorrer o arquivo de
     * entrada.
     */
    public AnalisadorLexico(LeitorEntrada leitor) {
        this.tpr = new TabelaReservados();
        this.leitor = leitor;
    }

    /**
     * Obtém o leitor de arquivo.
     *
     * @return leitor
     */
    public LeitorEntrada getLeitor() {
        return leitor;
    }

    /**
     * Método que gerencia o reconhecimento de tokens.
     *
     * @return proximo token encontrado no arquivo de entrada ou null se não tem
     * mais tokens no arquivo de entrada.
     * @throws LexicoException
     */
    public Token proximoToken() {
        char c = leitor.proximoCaracter();
        Token t = null;

        /*
         * verifica se existem vários comentários ou espaços em branco no
         * arquivo de entrada para ter certeza que o próximo caracter encontrado
         * formará um token ou que o arquivo terminou.
         */
        while (c == ' ' || c == '\n' || c == '{' || c == '\t' || c == '\r' || c == '\f') {
            /*
             * verifica a existência de comentários caso encontre um char igual
             * a "{", a chamada de função para reconheceComentario() irá
             * consumir char a char até encontrar o "}" correspondente ou um
             * "\n", indicando erro de comentário não fechado.
             */
            if (c == '{') {
                reconheceComentario();
                c = leitor.proximoCaracter();
            } else {
                /*
                 * verifica se existem espaços em branco no arquivo de entrada,
                 * pois estes devem ser ignorados pelo analisador léxico.
                 */
                c = leitor.proximoCaracter(); //ignora espaços em branco.
            }
        }
        /*
         * verifica se toda a entrada já foi lida. Verificação necessária para
         * casos em que existem somente espaços em branco no final do arquivo ou
         * comentários. Então nenhum token será encontrado retornando o valor
         * null.
         */
        if (leitor.terminou()) {
            return t;
        }

        //tenta reconhecer um token baseado no caracter atual.
        if (Character.isDigit(c)) {
            leitor.retrocedeEntrada();
            t = reconheceNumero();
        } else if (Character.isLetter(c) || c == '_') {
            leitor.retrocedeEntrada();
            t = reconheceIdentificador();
        } else if (c == '"') {
            leitor.retrocedeEntrada();
            t = reconheceCadeiaLiteral();
        } else {
            leitor.retrocedeEntrada();
            t = reconheceSimboloReservado();
        }

        //se nenhum token foi encontrado acima, então o caracter atual não pode ser reconhecido, gerando uma exceção.
        if (t == null) {
            String mensagem = "Linha " + leitor.getLinha() + ": " + c + " - simbolo nao identificado";
            throw new LexicoException(mensagem);
        }

        return t;
    }

    /**
     * Método que reconhece um comentário e o ignora.
     *
     * @throws LexicoException se ocorrer um comentário não fechado.
     */
    public void reconheceComentario() {
        char c;
        c = leitor.proximoCaracter();

        //enquanto não encontrar } ou final de arquivo ou mudança de linha, consome a entrada
        while (c != '\n' && c != '}' && c != '\0') {
            c = leitor.proximoCaracter();
        }

        //se encontrou mudança de linha ou final de arquivo, então ocorreu um erro de comentário não fechado
        if (c == '\n' || c == '\0') {
            String mensagem = "Linha " + (leitor.getLinha() + 1) + ": comentario nao fechado";
            //lança uma exceção com a mensagem específica para indicar o erro
            throw new LexicoException(mensagem);
        }
    }

    /**
     * Método que reconhece um simbolo reservado.
     *
     * @return token correspondente ao simbolo encontrado ou null se o simbolo
     * não for reconhecido.
     */
    public Token reconheceSimboloReservado() {
        StringBuilder buffer = new StringBuilder();  //buffer que armazenará o lexema
        int estado = 1; //variável que controla o estado do autômato de reconhecimento
        char c;

        while (estado != 0) {
            switch (estado) {
                case 1:
                    c = leitor.proximoCaracter();
                    buffer.append(c);
                    /*
                     * Caso o símbolo tenha mais de um caractere, um outro
                     * estado é chamado para verificar isso
                     */
                    if (c == '<' || c == '>' || c == '.') {
                        estado = 2;
                    } else {  // se for símbolo de um só caractere, reconhece aqui                       
                        estado = 0;
                        String lexema = buffer.toString();
                        TipoToken tipo = tpr.busca(lexema);
                        if (tipo != null) {
                            Token t = new Token(tipo, lexema);
                            return t;
                        } else {
                            return null;
                        }
                    }
                    break;

                case 2:
                    c = leitor.proximoCaracter();
                    /*
                     * É verificado se o proximo caracter realmente forma um
                     * símbolo válido. Se não formar, então retrocede a leitura
                     * e retorna o símbolo encontrado no estado anterior.
                     */
                    if (c == '-' && buffer.charAt(0) == '<') {
                        buffer.append(c); //coloca no buffer o segundo caractere do símbolo                        
                        estado = 0;
                        String lexema = buffer.toString();
                        TipoToken tipo = tpr.busca(lexema);
                        Token t = new Token(tipo, lexema);
                        return t;
                    } else if (c == '>' && buffer.charAt(0) == '<') {
                        buffer.append(c); //coloca no buffer o segundo caractere do símbolo
                        estado = 0;
                        String lexema = buffer.toString();
                        TipoToken tipo = tpr.busca(lexema);
                        Token t = new Token(tipo, lexema);
                        return t;
                    } else if ((c == '=') && (buffer.charAt(0) == '>' || buffer.charAt(0) == '<')) {
                        buffer.append(c); //coloca no buffer o segundo caractere do símbolo
                        estado = 0;
                        String lexema = buffer.toString();
                        TipoToken tipo = tpr.busca(lexema);
                        Token t = new Token(tipo, lexema);
                        return t;
                    } else if (c == '.' && buffer.charAt(0) == '.') {
                        buffer.append(c); //coloca no buffer o segundo caractere do símbolo
                        estado = 0;
                        String lexema = buffer.toString();
                        TipoToken tipo = tpr.busca(lexema);
                        Token t = new Token(tipo, lexema);
                        return t;
                    } else {
                        leitor.retrocedeEntrada();
                        estado = 0;
                        String lexema = buffer.toString();
                        TipoToken tipo = tpr.busca(lexema);
                        Token t = new Token(tipo, lexema);
                        return t;
                    }
            }
        }
        return null;
    }

    /**
     * Método que reconhece um identificador ou palavra reservada.
     *
     * @return token correspondente ao identificador ou palavra reservada
     * encontrada.
     */
    public Token reconheceIdentificador() {
        StringBuilder buffer = new StringBuilder();  //buffer que armazenará o lexema
        int estado = 1; //variável que controla o estado do autômato de reconhecimento
        char c;

        while (estado != 0) {
            switch (estado) {
                case 1:
                    c = leitor.proximoCaracter();
                    estado = 2;  //vai para o próximo estado de reconhecimento
                    buffer.append(c);  //salva o primeiro caracter do token no buffer.
                    break;

                case 2:
                    c = leitor.proximoCaracter();
                    /*
                     * enquanto o proximo caracter não for diferente de número,
                     * letra ou underscore permanece no mesmo estado e salva o
                     * caracter no buffer.
                     */
                    if (c == '_' || Character.isLetter(c) || Character.isDigit(c)) {
                        buffer.append(c);
                    } else {
                        /*
                         * neste ponto um token foi reconhecido, então retrocede
                         * a entrada, muda para um estado final, e verifica se o
                         * que foi encontrado é um identificador ou uma palavra
                         * reservada.
                         */
                        leitor.retrocedeEntrada();
                        estado = 0;
                        String lexema = buffer.toString();
                        //verifica se o lexema salvo no buffer é uma palavra
                        // ou símbolo reservado.
                        TipoToken tipo = tpr.busca(lexema);
                        // é palavra/símbolo reservado
                        if (tipo != null) {
                            Token t = new Token(tipo, lexema);
                            return t;
                        } // não é palavra/símbolo reservado (é um identificador)
                        else {
                            Token t = new Token(TipoToken.Identificador, lexema);
                            return t;
                        }
                    }
                    break;
            }
        }

        return null;
    }

    /**
     * Método que reconhece um número inteiro ou real.
     *
     * @return token correspondente ao número inteiro ou real encontrado.
     */
    public Token reconheceNumero() {
        StringBuilder buffer = new StringBuilder();  //buffer que armazenará o lexema
        int estado = 1; //variável que controla o estado do autômato de reconhecimento

        char c = leitor.proximoCaracter();
        buffer.append(c);

        while (estado != 0) {
            switch (estado) {
                case 1:
                    /*
                     * Vai lendo a entrada até encontrar um ".", que indica que
                     * o número pode ser real; ou caso não encontre o ".",
                     * reconhece o número lido como inteiro
                     */
                    c = leitor.proximoCaracter();
                    if (Character.isDigit(c)) {
                        buffer.append(c);
                    } else if (c == '.') {  //possível número real                                            
                        buffer.append(c);
                        estado = 2;
                    } else {
                        leitor.retrocedeEntrada();
                        estado = 0;
                        String lexema = buffer.toString();
                        Token t = new Token(TipoToken.NumeroInteiro, lexema);
                        return t;
                    }
                    break;

                case 2:
                    c = leitor.proximoCaracter();
                    //se o próximo caracter depois do . for um dígito então o número é real
                    if (Character.isDigit(c)) {
                        buffer.append(c);
                        estado = 3;
                    } else {
                        /*
                         * neste caso o número será inteiro e o . achado
                         * anteriormente terá que ser tratado no próximo token.
                         * então a posição de leitura é retrocedida em duas
                         * posições e o último caracter do buffer é apagado para
                         * retornar o lexema correto.
                         */
                        leitor.retrocedeEntrada(2);
                        buffer.deleteCharAt(buffer.length() - 1);
                        estado = 0;
                        String lexema = buffer.toString();
                        Token t = new Token(TipoToken.NumeroInteiro, lexema);
                        return t;
                    }
                    break;

                case 3:

                    //o número com certeza é real, então enquanto tiver mais dígitos, adiciona no buffer
                    c = leitor.proximoCaracter();
                    if (Character.isDigit(c)) {
                        buffer.append(c);
                    } else {
                        leitor.retrocedeEntrada();
                        estado = 0;
                        String lexema = buffer.toString();
                        Token t = new Token(TipoToken.NumeroReal, lexema);
                        return t;
                    }
                    break;
            }
        }
        return null;
    }

    /**
     * Método que reconhece uma cadeia literal.
     *
     * @return token correspondente à cadeia literal encontrada.
     */
    public Token reconheceCadeiaLiteral() {
        StringBuilder buffer = new StringBuilder();  //buffer que armazenará o lexema
        int estado = 1; //variável que controla o estado do autômato de reconhecimento        

        char c = leitor.proximoCaracter();
        buffer.append(c);

        while (estado != 0) {
            switch (estado) {
                case 1:
                    c = leitor.proximoCaracter();
                    if (c == '"') {  //encontrou o final da cadeia, muda de estado para reconhecer
                        buffer.append(c);
                        estado = 2;
                    } else if (c == '\n' || c == '\0') {
                        /*
                         * encontrou o final de uma linha ou arquivo, mas não
                         * achou o final da cadeia. então retrocede a leitura do
                         * arquivo no tamanho do lexema reconhecido até o
                         * momento e retorna null, pois não foi possível achar
                         * um token. dentro do método proximoToken isso será
                         * tratado como um erro, gerando o caracter inválido
                         * '"'.
                         */
                        leitor.retrocedeEntrada(buffer.length());
                        return null;
                    } else {
                        //vai adicionando os caracteres no buffer enquanto não
                        //encontrar um " ou final de linha ou final de arquivo
                        buffer.append(c);
                    }
                    break;

                case 2:
                    //reconhece o token de cadeia de caracteres
                    estado = 0;
                    String lexema = buffer.toString();
                    Token t = new Token(TipoToken.CadeiaLiteral, lexema);
                    return t;
            }
        }
        return null;
    }
}